package blob

import (
	"context"
	blobpb "coolcar/blob/api/gen/v1"
	"coolcar/blob/dao"
	"coolcar/shared/id"
	"io"
	"io/ioutil"
	"net/http"
	"time"

	"go.mongodb.org/mongo-driver/mongo"
	"go.uber.org/zap"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
)

// Storage defines storage interface.
type Storage interface {
	SignURL(ctx context.Context, method, path string, timeout time.Duration) (string, error)
	Get(ctx context.Context, path string) (io.ReadCloser, error)
}

// service defines a blob service.
type service struct {
	Storage
	Mongo  *dao.Mongo
	Logger *zap.Logger
	blobpb.UnimplementedBlobServiceServer
}

// Service instantiation service
func Service(storage Storage, mongo *dao.Mongo, log *zap.Logger) *service {
	return &service{
		Storage: storage,
		Mongo:   mongo,
		Logger:  log,
	}
}

// CreateBlob creates a blob.
func (s *service) CreateBlob(ctx context.Context, req *blobpb.CreateBlobRequest) (*blobpb.CreateBlobResponse, error) {
	aid := id.AccountID(req.AccountId)
	br, err := s.Mongo.CreateBlob(ctx, aid)
	if err != nil {
		// CreateBlob 应该是不太会错的，只是创建一条记录，数据库又没有索引什么的，但是它出错了就很怪了，因此需要写个日志
		s.Logger.Error("cannot create blob", zap.Error(err))
		return nil, status.Error(codes.Internal, "")
	}

	// 我们希望SignURL既可签名上传的URL也能签名下载的URL，不如把method传给它。
	// UploadUrlTimeoutSec我们应该把它转成Duration，因此写一个secToDuration函数。
	// 返回 预签名url + err
	u, err := s.Storage.SignURL(ctx, http.MethodPut, br.Path, secToDuration(req.UploadUrlTimeoutSec))
	if err != nil {
		// 这个签名出错可以回给用户，我们不希望的是把MongoDB的内部实现回给用户，但是签名很可能是腾讯云那边出错的。
		return nil, status.Errorf(codes.Aborted, "cannot sign url :%v", err)
	}

	return &blobpb.CreateBlobResponse{
		Id:        br.ID.Hex(),
		UploadUrl: u,
	}, nil
}

// GetBlob gets a blob's contents.
func (s *service) GetBlob(ctx context.Context, req *blobpb.GetBlobRequest) (*blobpb.GetBlobResponse, error) {
	br, err := s.getBlobRecord(ctx, id.BlobID(req.Id))
	if err != nil {
		return nil, err
	}

	r, err := s.Storage.Get(ctx, br.Path)
	if r != nil {
		defer r.Close()
	}
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "cannot get storage: %v", err)
	}

	// 因为这里*blobpb.GetBlobResponse返回的是Data []byte类型，因此还是要读
	b, err := ioutil.ReadAll(r)
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "cannot read from response: %v", err)
	}

	return &blobpb.GetBlobResponse{
		Data: b,
	}, nil
}

// GetBlobURL gets blob's URL for downloading.
func (s *service) GetBlobURL(ctx context.Context, req *blobpb.GetBlobURLRequest) (*blobpb.GetBlobURLResponse, error) {
	br, err := s.getBlobRecord(ctx, id.BlobID(req.Id))
	if err != nil {
		return nil, err
	}

	u, err := s.Storage.SignURL(ctx, http.MethodGet, br.Path, secToDuration(req.TimeoutSec))
	if err != nil {
		return nil, status.Errorf(codes.Aborted, "cannot sign url :%v", err)
	}

	return &blobpb.GetBlobURLResponse{
		Url: u,
	}, nil
}

func (s *service) getBlobRecord(ctx context.Context, bid id.BlobID) (*dao.BlobRecord, error) {
	br, err := s.Mongo.GetBlob(ctx, bid)
	if err == mongo.ErrNoDocuments {
		// 找不到document返回NotFound
		return nil, status.Error(codes.NotFound, "")
	}
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, err.Error())
	}
	return br, nil
}

func secToDuration(sec int32) time.Duration {
	// time.Duration 本来很好用，本来说10s就是10*time.Second。
	// 变量后面要跟单位，go语言的Duration，就直接是带一个时间单位的。
	// 我们不会说一个10，我们不知道它是10秒还是10毫秒的问题。
	// 但是它有一点小问题，我们的 sec * time.Second 它会报错。
	// 因此必须要写成 time.Duration(sec)，给它转一下。
	return time.Duration(sec) * time.Second
}
